package org.haptimap.hcimodules.tactilecompass;

import java.util.ArrayList;
import java.util.List;

import android.content.Context;
import android.location.Location;
import android.util.Log;

/**
 * The bearing module computes the general direction of a POI in relation to the
 * user's orientation and heading. It will be part of the HaptiMap toolkit.
 * @author <a href="mailto:"martin.pielot@offis.de">Martin Pielot</a>
 * @version 05.08.2010
 */
public class BearingModule implements HeadingModuleListener {

	// ========================================================================
	// Constant Fields
	// ========================================================================

	private static final String			TAG			= "BearingModule";

	// ========================================================================
	// Fields
	// ========================================================================

	private HeadingModule				headingModule;
	private List<BearingModuleListener>	listeners	= new ArrayList<BearingModuleListener>();

	// Externally accessible data
	private Location					destination;
	private double						heading;
	private int							relativeDirection;

	// ========================================================================
	// Constructor
	// ========================================================================

	public BearingModule(Context context) {
		this.headingModule = new HeadingModule(context, this);
	}

	// ========================================================================
	// Methods
	// ========================================================================

	public void addListener(BearingModuleListener listener) {
		this.listeners.add(listener);
	}

	public void setDestination(Location destination) {
		this.destination = destination;
		this.updateRelativeDirection();
		Log.i(TAG, "new destination at bearing " + relativeDirection);
	}

	public int getRelativeDirection() {
		return relativeDirection;
	}

	public boolean hasValidData() {
		return destination != null && headingModule.hasValidData();
	}

	public boolean hasDestination() {
		return destination != null;
	}

	public HeadingModule getHeadingModule() {
		return headingModule;
	}

	/**
	 * Returns seconds until reaching destination with current speed. Will
	 * return 0 if time cannot be computed.
	 */
	public double timeUntilDestination() {

		Location userLocation = headingModule.getUserLocation();
		if (destination == null || userLocation == null)
			return 0;

		if (userLocation.getAccuracy() > HeadingModule.MIN_WALKING_GPS_ACCURACY)
			return 0;

		// speed is meters/second
		float speed = userLocation.getSpeed();
		if (speed == 0)
			return 0;

		// distance is meters
		float distance = destination.distanceTo(userLocation);

		// should be seconds then
		float timeUntilDest = distance / speed;

		// Log.i(TAG,
		// Util.format(speed)+", "+Util.format(distance)+" = "+Util.format(timeUntilDest));

		return timeUntilDest;
	}

	// ------------------------------------------------------------------------
	// Implementation of 'HeadingModuleListener'
	// ------------------------------------------------------------------------

	/** Called when user heading has changed. */
	public void onHeadingChanged(double heading, boolean hasSufficientAccuracy) {
		// Log.d(TAG, "onHeadingChanged( "
		// + heading
		// + ", "
		// + hasSufficientAccuracy
		// + " )");
		this.heading = heading;
		this.updateRelativeDirection();
	}

	/** User just started pointing. */
	public void onStartPointing() {
		this.updateRelativeDirection();

		for (BearingModuleListener listener : listeners)
			listener.onStartScanningMode();
	}

	// ------------------------------------------------------------------------
	// Updating
	// ------------------------------------------------------------------------

	private void updateRelativeDirection() {

		if (!hasDestination()) {
			return;
		}

		Location userLocation = headingModule.getUserLocation();
		if (userLocation == null)
			return;

		// Obtain the azimuth between the user's location and the destination
		double bearingTo = userLocation.bearingTo(destination);
		bearingTo = modDouble(bearingTo, 360);

		// Calculate the angle difference between the user's heading and the
		// bearing of the destination.
		int diff = (int) difference(heading, bearingTo);
		int relDir = mod(diff, 360);

		if (relDir != relativeDirection) {
			this.relativeDirection = relDir;
			for (BearingModuleListener listener : listeners)
				listener.onBearingChanged();
		}
	}

	// ------------------------------------------------------------------------
	// Helpers
	// ------------------------------------------------------------------------

	private static double modDouble(double d, int ring) {
		while (d < ring)
			d += ring;

		while (d >= ring)
			d -= ring;
		return d;
	}

	/**
	 * Applies a mathematically correct mod operation to v. It will not return
	 * negative values.
	 */
	private static int mod(int v, int ring) {
		v %= ring;
		if (v < 0)
			v += ring;
		return v;
	}

	/**
	 * Calculates the difference between two angles, e.g. the trajectory and the
	 * azimuth between the user and another place.
	 * @param referenceAngle the reference angle
	 * @param otherAngle the second angle
	 * @return the signed difference between the to angle, seen from the first
	 *         angle
	 */
	private static double difference(double referenceAngle, double otherAngle) {

		double difference = otherAngle - referenceAngle;

		if (difference > 180)
			difference = difference - 360;

		if (difference <= -180)
			difference = difference + 360;

		return difference;
	}

}
